package classedpack

import (
	"bytes"
	"context"
	"encoding/hex"
	"fmt"
	"io"
	"log"
	"os"
	"path"
	"sort"
	"sync"
	"time"

	"gitee.com/thubcc/p2pgit/classify"
	"gitee.com/thubcc/p2pgit/constant"
	"gitee.com/thubcc/p2pgit/format"
	"gitee.com/thubcc/p2pgit/types"
	"gitee.com/thubcc/p2pgit/utils"
	"github.com/gin-gonic/gin"
	"gopkg.in/src-d/go-git.v4/plumbing"
)

// File pack file with sepcial classifier
type File struct {
	C        types.Classifer
	Dir      string
	rlock    sync.Mutex
	wlock    sync.Mutex
	idx      format.Indexes
	rf       *os.File
	wf       *os.File
	w        bool
	splited  bool
	chirdren types.SomeRawer
	pos      int64
	has      func(types.Selector) bool
	syncTrig chan int
	drop     chan int
	cache    format.Indexes
}

func hashFromSelector(s types.Selector) (h types.Hash) {
	h = types.ZeroHash
	switch hs := s.(type) {
	case types.FullSelect:
		copy(h[:], hs[:20])
	case types.HashSelect:
		h = types.Hash(hs)
	}
	return
}
func (c *File) defaultHas(s types.Selector) bool {
	_, cerr := c.getByHash(hashFromSelector(s), s)
	if cerr != types.ErrorNoFind {
		if cerr != nil {
			cerr = utils.WarpError(cerr, "has")
			filelog.Println(cerr)
		}
		return true
	} else {
		return false
	}
}

// Name report file name by classifier
func (c *File) Name() string {
	return path.Join(c.Dir, "np-"+c.C.String())
}

// Reset reset has fuction
func (c *File) Reset() error {
	c.rlock.Lock()
	defer c.rlock.Unlock()
	c.has = nil
	return nil
}

var (
	idxSuffix       = ".nidx"
	packSuffix      = ".npack"
	examplePack     = "np-08-0000000000000003" + packSuffix
	packFileNameLen = len(examplePack)
	filelog         *log.Logger
)

func init() {
	filelog = log.New(os.Stdout, "file:", 6)
}

func (c *File) maintain() {
	for {
		select {
		case <-time.After(time.Second * 10):
			c.sync()
		case <-c.syncTrig:
			for len(c.syncTrig) > 0 {
				<-c.syncTrig
			}
			c.sync()
		case <-c.drop:
			return
		}
	}
}

//Open open file, w true means Writeable, otherwise Readonly
func (c *File) Open(w bool) (err error) {
	if c.splited {
		return c.chirdren.Open(w)
	}
	c.w = w
	idxfn := c.Name() + idxSuffix
	packfn := c.Name() + packSuffix
	var stat os.FileInfo
	if stat, err = os.Stat(packfn); err != nil {
		if !w {
			err = fmt.Errorf("File no exist (%s)", packfn)
			return
		}
		c.wf, err = os.Create(packfn)
		if err != nil {
			return
		}
		c.pos = 0
	} else {
		if w {
			c.wf, err = os.OpenFile(packfn, os.O_APPEND|os.O_RDWR, 0644)
			if err != nil {
				return
			}
			c.pos = stat.Size()
		}
		c.rf, err = os.Open(packfn)
		if err != nil {
			return
		}
	}

	_, err = os.Stat(idxfn)
	if err == nil {
		var idxf *os.File
		idxf, err = os.Open(idxfn)
		if err != nil {
			return
		}
		defer idxf.Close()
		c.idx, err = format.IndexFromFile(idxf)
		if err != nil {
			return
		}
	} else {
		c.idx, err = c.BuildIdx()
		if err != nil {
			return
		}
	}
	if w {
		c.drop = make(chan int)
		c.syncTrig = make(chan int, 10)
		go c.maintain()
	}
	return
}

// BuildIdx build indexes from pack file
func (c *File) BuildIdx() (idxes format.Indexes, err error) {
	if c.splited {
		return format.Indexes{}, fmt.Errorf("file is splited")
	}
	err = format.TravelPackFile(c.Name()+packSuffix, func(o uint32, h *types.Header32, r io.ReadSeeker) error {
		idxes = append(idxes, format.ToIndex(format.KeyPart(c.C.IdxKey(h.Hash)), o))
		return nil
	})
	sort.Sort(idxes)
	return
}

// Hashes return all object of the pack
func (c *File) Hashes() (hs []types.Hash, err error) {
	if c.splited {
		return []types.Hash{}, fmt.Errorf("file is splited")
	}
	var hm = make(map[types.Hash]struct{})
	err = format.TravelPackFile(c.Name()+packSuffix, func(o uint32, h *types.Header32, r io.ReadSeeker) error {
		hm[h.Hash] = struct{}{}
		return nil
	})
	for k := range hm {
		hs = append(hs, k)
	}

	return
}

// Sync write to file
func (c *File) Sync() (err error) {
	if c.splited {
		return c.chirdren.Sync()
	}
	return c.sync()
}

func (c *File) sync() (err error) {
	c.rlock.Lock()
	defer c.rlock.Unlock()
	c.wlock.Lock()
	defer c.wlock.Unlock()

	if c.w && c.wf != nil {
		err = c.wf.Sync()
		if err != nil {
			return
		}
	}

	var w *os.File
	w, err = os.Create(c.Name() + idxSuffix)
	if err != nil {
		return
	}
	sort.Sort(c.idx)
	err = c.idx.ToFile(w)
	return
}

// Close write index to .idx
func (c *File) Close() (err error) {
	if c.splited {
		return c.chirdren.Close()
	}
	return c.close()
}
func (c *File) close() (err error) {
	c.Sync()
	if c.w && c.wf != nil {
		err = c.wf.Close()
		c.wf = nil
		c.w = false
		if err != nil {
			return
		}
	}
	if c.rf != nil {
		err = c.rf.Close()
		c.rf = nil
	}
	return
}

// GetByHash find out sepcial object with hash and selector
func (c *File) GetByHash(hash types.Hash, sel types.Selector) (ho *types.HeadOffset, err error) {
	if c.splited {
		return nil, fmt.Errorf("file is splited")
	}
	return c.getByHash(hash, sel)
}

func (c *File) getByHash(hash types.Hash, sel types.Selector) (ho *types.HeadOffset, err error) {
	var key format.KeyPart
	var head []*types.HeadOffset
	if c.C.Hit(hash) {
		key = c.C.IdxKey(hash)
		p := c.idx.Find(key)
		for _, o := range p {
			var do = func() (cerr error) {
				c.rlock.Lock()
				defer c.rlock.Unlock()
				_, cerr = c.rf.Seek(int64(o), 0)
				if cerr != nil {
					return utils.WarpError(cerr, fmt.Sprint("Seek @", o, c.Name()))
				}
				var h32 *types.Header32
				h32, cerr = types.NewHeader32FromFile(c.rf)
				if cerr != nil {
					return utils.WarpError(cerr, "Header from file")
				}
				if h32.Hash == hash {
					head = append(head, &types.HeadOffset{
						Head:   *h32,
						Offset: o,
					})
				}
				return
			}
			err = do()
			if err != nil {
				return
			}
		}
	}
	if len(head) == 0 {
		err = types.ErrorNoFind
		return
	}
	ho = sel.Determine(head)
	if ho == nil {
		err = types.ErrorNoFind
	}
	return
}

// Hit interface
func (c *File) Hit(h types.Hash) bool {
	if c.splited {
		return c.chirdren.Hit(h)
	}
	return c.C.Hit(h)
}

// GetRaw get raw object with sepcial selector
func (c *File) GetRaw(h types.Selector) (raw []byte, err error) {
	if c.splited {
		return c.chirdren.GetRaw(h)
	}

	var fo *types.HeadOffset
	var hash types.Hash

	switch hs := h.(type) {
	case types.HashSelect:
		hash = types.Hash(hs)
	case types.FullSelect:
		var o *types.Header32
		var h256 = types.Hash256(hs)
		o, err = h256.ToHeader32()
		if err != nil {
			return
		}
		hash = o.Hash
	}
	fo, err = c.GetByHash(hash, h)
	if err != nil {
		return
	}
	raw = make([]byte, int(fo.Head.Size+32))
	c.rlock.Lock()
	defer c.rlock.Unlock()
	_, err = c.rf.Seek(int64(fo.Offset), 0)
	if err != nil {
		return
	}
	_, err = io.ReadFull(c.rf, raw)
	if err == io.EOF {
		err = nil
	}
	return
}

// GetAsync get raw object with sepcial selector
func (c *File) GetAsync(ctx context.Context, h types.Selector, cb func(interface{}) error) context.CancelFunc {
	var raw []byte
	var err error
	sctx, cancel := context.WithCancel(context.TODO())
	go func() {
		raw, err = c.GetRaw(h)
		cancel()
	}()
	select {
	case <-ctx.Done():
		return cancel
	case <-sctx.Done():
		if err == types.ErrorNoFind {
			return func() {}
		}
		if err != nil {
			fmt.Println(h.String(), err)
			err = cb(err)
			return cancel
		}
		var r interface{} = raw
		err = cb(r)
		return cancel
	}
}

// Get get object with sepcial selector
func (c *File) Get(h types.Selector) (uncompressed []byte, t plumbing.ObjectType, hb *types.Header32, err error) {
	var raw []byte
	raw, err = c.GetRaw(h)
	if err != nil {
		return
	}
	return format.Parse(raw)
}

func (c *File) add(h types.Hash256, o uint32) {
	h32, err := h.ToHeader32()
	if err == nil {
		c.cache = append(c.cache, format.ToIndex(c.C.IdxKey(h32.Hash), o))
		if c.cache.Len() > constant.MaxCacheObjNum {
			c.idx = append(c.idx, c.cache...)
			c.cache = format.Indexes{}
			sort.Sort(c.idx)
			c.wf.Sync()
			c.syncTrig <- 1
		}
	}
}

// Put append new object at file end
func (c *File) Put(raw []byte) error {
	if int64(len(raw)) > constant.MaxObjectSize {
		return nil
	}
	if !c.w && !c.splited {
		return fmt.Errorf("pack file read only")
	}
	var h types.Hash
	copy(h[:], raw[:20])
	if !c.C.Hit(h) {
		return nil
	}

	err := func() error {
		c.wlock.Lock()
		defer c.wlock.Unlock()
		if c.splited {
			return c.chirdren.Put(raw)
		}
		var h32 types.Hash256
		copy(h32[:], raw[:32])
		var cerr error
		if c.has != nil {
			if c.has(types.FullSelect(h32)) {
				return nil
			}
		}
		if c.defaultHas(types.FullSelect(h32)) {
			return nil
		}
		_, cerr = c.wf.Seek(c.pos, 0)
		if cerr != nil {
			return utils.WarpError(cerr, "Seek")
		}
		_, cerr = c.wf.Write(raw)
		if cerr != nil {
			return utils.WarpError(cerr, "Write")
		}

		c.add(h32, uint32(c.pos))

		c.pos += int64(len(raw))
		if c.pos > constant.MaxPackFileSize {
			cerr = c.Split()
			if cerr != nil {
				return utils.WarpError(cerr, "Split")
			}
		}
		return nil
	}()
	if err != nil {
		fmt.Println("put:", err)
		return err
	}
	return nil
}

//CheckPackFileName check pack file name
func CheckPackFileName(base string) (err error) {
	if packFileNameLen != len(base) {
		err = fmt.Errorf("Bad file name: %s", base)
		return
	}
	if base[packFileNameLen-len(packSuffix):] != packSuffix {
		err = fmt.Errorf("Bad file name: %s", base)
		return
	}
	if base[:3] != examplePack[:3] {
		err = fmt.Errorf("Bad file name: %s", base)
		return
	}
	return nil
}

// NewFile Classed Pack File form file name
func NewFile(fn string) (cpf *File, err error) {
	dir := path.Dir(fn)
	base := path.Base(fn)
	err = CheckPackFileName(base)
	if err != nil {
		return
	}
	var cpfs = base[3 : packFileNameLen-len(packSuffix)]
	var c *classify.BasicClassify
	c, err = classify.NewBasicClassifyFromString(cpfs)
	if err != nil {
		return
	}
	return &File{
		C:   c,
		Dir: dir,
	}, nil
}

// Split split to two low level
func (c *File) Split() (err error) {
	bc, ok := c.C.(*classify.BasicClassify)
	if !ok {
		err = fmt.Errorf("Not Support: %s", c.Name())
		return
	}
	var ops []types.Rawer
	for i := 0; i < 2; i++ {
		var r types.Rawer = &File{
			C: &classify.BasicClassify{
				Prefix: bc.Prefix*2 + uint64(i),
				BitLen: bc.BitLen + 1,
			},
			Dir: c.Dir,
			has: c.defaultHas,
		}
		ops = append(ops, r)
	}
	c.chirdren = types.NewRawer(ops)
	err = c.chirdren.Open(true)
	c.splited = true
	if err != nil {
		fmt.Println(err, c.Name())
	}
	fmt.Println("begin split", c.chirdren.Name())
	go c.split()
	return
}

func (c *File) split() {
	var err error
	err = format.TravelPackFile(c.Name()+packSuffix, func(o uint32, h *types.Header32, r io.ReadSeeker) error {
		var buf = make([]byte, h.Size+32)
		h32 := h.ToHash256()
		copy(buf[:32], h32[:])
		io.ReadFull(r, buf[32:])
		c.chirdren.Put(buf)
		return nil
	})
	if err != nil {
		filelog.Println("exit split", err)
	}
	filelog.Println("End split", c.Name(), c.chirdren.Name())
	err = c.close()
	if err != nil {
		filelog.Println(err)
	}

	c.chirdren.Reset()
	c.cache = format.Indexes{}
	close(c.drop)
	err = c.Remove()
	if err != nil {
		filelog.Println(err)
	}
	if _, err = os.Stat(c.Name() + packSuffix); err == nil {
		fmt.Println("End split", c.Name(), "pack file not remove")
	}
	if _, err = os.Stat(c.Name() + idxSuffix); err == nil {
		fmt.Println("End split", c.Name(), "idx file not remove")
	}
}

// Remove remove index and pack file
func (c *File) Remove() (err error) {
	idxfn := c.Name() + idxSuffix
	packfn := c.Name() + packSuffix
	if _, err = os.Stat(idxfn); err == nil {
		err = os.Remove(idxfn)
		if err != nil {
			return
		}
	}
	if _, err = os.Stat(packfn); err == nil {
		return os.Remove(packfn)
	}
	return nil
}

// Handle Http handle
func (c *File) Handle(e *gin.Engine) {
	var path = path.Base(c.Name())
	var getHandle = func(gc *gin.Context) {
		hs := gc.Query("hash")
		var one = func() (err error) {
			var s types.Selector
			switch len(hs) {
			case 0:
				err = fmt.Errorf("miss hash")
			case 64:
				var h32 types.Hash256
				var buf []byte
				buf, err = hex.DecodeString(hs)
				if err == nil {
					copy(h32[:], buf)
					s = types.FullSelect(h32)
				}
			case 40:
				var hash types.Hash
				var buf []byte
				buf, err = hex.DecodeString(hs)
				if err == nil {
					copy(hash[:], buf)
					s = types.HashSelect(hash)
				}
			default:
				err = fmt.Errorf("invalid hash length")
			}
			if err != nil {
				return
			}
			var raw []byte
			raw, err = c.GetRaw(s)
			if err != nil {
				err = fmt.Errorf("get %s: %v", s.String(), err)
				return
			}
			r := bytes.NewReader(raw)
			var h *format.Header
			h, err = format.ExtFromFile(r)
			if err != nil {
				return
			}
			var hh = h.ToMap()
			gc.DataFromReader(200, int64(h.Header32.Size)+32-int64(h.ByteLen()), "application/octet-stream", r, hh)
			return
		}
		err := one()
		if err != nil {
			fmt.Fprintln(gin.DefaultWriter, "Get", err)
			gc.JSON(400, gin.H{"error": err.Error()})
			return
		}
		return
	}
	var putHandle = func(pc *gin.Context) {
		var err error
		cl := int(pc.Request.ContentLength)
		var raw = make([]byte, cl)
		_, err = io.ReadFull(pc.Request.Body, raw)
		if err != nil {
			pc.JSON(400, gin.H{"error": err.Error()})
			return
		}
		pc.Request.Body.Close()
		err = format.TravelRaw(raw, func(h *types.Header32, o []byte) error {
			return c.Put(o)
		})
		if err != nil {
			fmt.Fprintln(gin.DefaultWriter, "Put", err)
			pc.JSON(400, gin.H{"error": err.Error()})
			return
		}
		pc.JSON(200, gin.H{"error": "nil"})
	}
	c.Open(true)
	e.GET(path, getHandle)
	e.POST(path, putHandle)
	go func() {
		for {
			<-time.After(constant.SyncTimer)
			c.Sync()
		}
	}()
}
